What is Amazon Lex?

Build production-grade conversational AI without ML expertise

Amazon Lex is AWS's fully managed, low-code service for building conversational interfaces — chatbots and voice bots — using the same deep learning technology that powers Alexa. You define the conversation logic visually or via JSON/API, and AWS handles all the NLP, ASR (speech recognition), and infrastructure.

🧠
NLU Built-in
Intent recognition + entity extraction with zero ML training
🎙️
Voice + Text
Automatic speech recognition works out of the box
Serverless
No servers to manage, scales instantly with traffic
🔗
Lambda Ready
Hook into any backend via AWS Lambda for fulfillment
📱
Omnichannel
Slack, Facebook, Kendra, Connect, web widget
💰
Pay Per Use
$0.004/text request, $0.00065/speech second

Where Lex Sits in the AWS AI Stack

User (Web / Mobile / Voice)
Amazon Lex V2 (Intent + Slot NLU)
AWS Lambda (Business Logic)
or
Built-in Fulfillment
DynamoDB / RDS / APIs / Kendra

Lex vs Alternatives — When to Pick Lex

CriteriaAmazon LexDialogflow CXCopilot Studio
EcosystemAWS-nativeGoogle CloudMicrosoft 365
LLM IntegrationBedrock + ClaudeVertex AIAzure OpenAI
Voice (ASR)Built-in Polly/TranscribeDialogflow STTAzure Speech
Low-Code UIVisual flow builderPage/flow designerCanvas designer
Best forAWS workloads, Contact CenterGCP workloadsM365 enterprise
💡 Key Insight

Amazon Lex V2 (current version) introduced a visual conversation flow designer and multi-bot aliasing — the older V1 API is deprecated for new projects. Always use V2.

Core Concepts

The vocabulary you must know to build anything in Lex

🤖 Bot

Top-level container. Holds all intents, slot types, and configurations. Has a name, language setting, and IAM role.

🎯 Intent

Represents what a user wants to do. E.g., OrderPizza, CheckBalance, BookFlight.

💬 Utterance

Sample phrases users might say to trigger an intent. "I want a pizza", "Order me a large margherita".

📦 Slot

A piece of data Lex needs to collect. Like a form field. E.g., PizzaSize, ToppingType.

🏷️ Slot Type

Defines valid values for a slot. Built-in types (dates, numbers) or custom enumerations you define.

⚙️ Fulfillment

What happens after all slots are filled. Either a Lambda hook or a simple static response message.

The Conversation Lifecycle

User says something
Lex NLU: Match Intent
Elicit Missing Slots
All Slots Filled?
→ yes →
Fulfillment (Lambda / Static)
↓ no
Ask user for missing slot value

Confidence Score & Fallback

Every intent match has a confidence score (0–1). If no intent passes the threshold, Lex triggers the built-in FallbackIntent. You can configure what happens there — route to an agent, ask for clarification, or connect to Amazon Bedrock for LLM-generated responses.

📐 Architecture Tip

Think of Lex as a router + form filler. It's excellent at extracting structured data from natural language. For open-ended chat or reasoning, chain it with Bedrock/Claude via Lambda.

Bot Versioning & Aliases

ConceptPurposeExample
DraftWorking version, not liveYour dev sandbox
VersionImmutable snapshotVersion 1, 2, 3...
AliasNamed pointer to a versionPROD → Version 3

Aliases let you do blue/green deployments: create a new version, test it under a staging alias, then switch the PROD alias with zero downtime.

🧠 Quick Check

What is an "Utterance" in Amazon Lex?
A. The data collected in a slot field
B. A sample phrase that triggers an intent
C. The Lambda function response
D. A type of bot deployment version
Utterances are example phrases you provide so Lex's NLU knows which intent to trigger. More diverse utterances = better recognition accuracy.

Console Walkthrough

Navigating the Amazon Lex V2 console from top to bottom

🔑 Prerequisites

AWS account with IAM user/role. Lex requires an IAM role with AmazonLexFullAccess + AWSLambdaRole if using Lambda fulfillment.

Console Sections at a Glance

SectionWhat you do there
BotsList, create, import/export bots
Bot > IntentsCreate intents, add utterances, configure slots
Bot > Slot typesDefine custom enumerated types with synonyms
Bot > Visual flowDrag-and-drop conversation graph editor
Bot > VersionsCreate immutable snapshots
Bot > AliasesMap aliases to versions, configure Lambda
Bot > Channel integrationsConnect to Slack, Facebook Messenger, etc.
Test console (inline)Chat in-browser to test NLU + dialogue

Step-by-Step: Creating a Bot

Open the Lex Console

Go to console.aws.amazon.com/lex → Select region (e.g. us-east-1) → Click Create bot.

Choose Bot Configuration Method

Select "Create a blank bot" for full control, or use a pre-built template (BookTrip, OrderFlowers). Templates are great for learning.

Set Bot Name + IAM Role

Give your bot a unique name. For IAM role: choose "Create a role with basic Amazon Lex permissions" for auto-setup.

Configure COPPA + Idle TTL

COPPA: No (for general apps). Idle session TTL: 5 minutes is standard. This controls how long sessions are remembered.

Select Language

English (US) is most feature-complete. Lex supports 10+ languages. You can add multiple languages to one bot.

Hit "Done" and Start Building

You'll land in the Intents list. Two intents are auto-created: FallbackIntent and you create the rest.

Your First Bot

A complete Pizza Ordering bot — from zero to working chatbot

We'll build PizzaBot — a bot that takes pizza orders. It will collect size, crust, and topping, then confirm the order. This teaches every core concept hands-on.

Bot Architecture Plan

Intent: OrderPizza
↓ requires slots
Slot: PizzaSize
+
Slot: CrustType
+
Slot: Toppings
↓ all collected
Lambda: process_order()
"Your {Size} pizza is on its way!"

Step 1: Define the Intent

In the Lex console under your bot → IntentsAdd intent → Name it OrderPizza.

Step 2: Add Utterances

Add at least 10 varied utterances. Lex learns from them to match user input:

# Sample utterances for OrderPizza intent
I want to order a pizza
Can I get a pizza please
Order me a {PizzaSize} pizza
I'd like a {PizzaSize} {CrustType} crust pizza
Get me a pizza with {Toppings}
I want a large pizza with extra cheese
Place a pizza order for me
Can you order a medium thin crust pizza
I'd like to place a pizza order
One pizza please
💡 Slot References in Utterances

Notice {PizzaSize} in utterances — Lex will extract the slot value directly from the phrase. This speeds up the conversation by skipping the "What size?" prompt.

Step 3: Create Slot Types

Before creating slots, define the custom types. Go to Slot typesAdd slot type:

# Slot Type: PizzaSizeType
Type:   Custom slot type (Expand values)
Values:
  - Small  (synonyms: sm, personal)
  - Medium (synonyms: med, regular)
  - Large  (synonyms: lg, big, family)
  - Extra Large (synonyms: XL, extra-large)

# Slot Type: CrustType
Values:
  - Thin      (synonyms: thin-crust, crispy)
  - Thick     (synonyms: deep-dish, pan)
  - Stuffed   (synonyms: cheese-stuffed)
  - Gluten-free

# Slot Type: ToppingType (AMAZON.AlphaNumeric also works)
Values:
  - Cheese, Pepperoni, Mushrooms, Onions
  - Olives, Peppers, Bacon, Pineapple

Step 4: Add Slots to the Intent

Back in your OrderPizza intent → Slots section → Add three slots:

Slot NameSlot TypePrompt (Lex asks this)Required?
PizzaSizePizzaSizeType"What size pizza would you like?"Yes
CrustTypeCrustType"What type of crust would you like?"Yes
ToppingsToppingType"What toppings would you like?"No

Step 5: Confirmation Prompt

Enable the Confirmation prompt in the intent settings:

Confirmation prompt:
"Just to confirm: a {PizzaSize} pizza with {CrustType} crust"
"and {Toppings}. Shall I place this order?"

Decline response:
"No problem, order cancelled. Let me know if you'd like anything else."

Step 6: Build & Test

Click Build (top right). Wait ~30 seconds. Then use the Test panel to have a conversation:

You:  I want a pizza
Bot:  What size pizza would you like?
You:  Large
Bot:  What type of crust would you like?
You:  Thin crust
Bot:  What toppings would you like?
You:  Pepperoni and mushrooms
Bot:  Just to confirm: a Large pizza with Thin crust and Pepperoni and mushrooms. Shall I place this order?
You:  Yes
Bot:  [Fulfillment response]

Intents & Utterances

Designing intents that understand natural human language

Intent Lifecycle States

StateMeaningYour action
InProgressCollecting slot valuesLex prompts user for each slot
ReadyForFulfillmentAll required slots filledTrigger Lambda or return response
FulfilledLambda/response returnedShow result to user
FailedLambda threw an errorShow error message

Built-in Intents

AMAZON.FallbackIntent

  • Triggered when no intent matches
  • Use to transfer to agent or trigger LLM
  • Always exists, can't be deleted

AMAZON.KendraSearchIntent

  • Auto-queries Amazon Kendra
  • Returns FAQ-style answers
  • Ideal for knowledge base bots

Utterance Best Practices

⚠️ Common Mistake

Don't add 3 utterances and wonder why NLU fails. Lex needs 15–30 diverse utterances per intent for robust recognition. Vary structure, not just wording.

# ❌ BAD - Too similar, low diversity
I want to book a flight
I would like to book a flight
Book me a flight

# ✅ GOOD - Structural variety
I want to book a flight
Can I get a ticket to {Destination}
Book me a {CabinClass} seat on {Date}
What flights are available to {Destination}
I need to fly to {Destination} on {Date}
Get me on a flight to {Destination}
Reserve a seat for me
I'd like to make a flight reservation
Schedule a flight for next {Date}
{Destination} please, flying {Date}

Intent Priority & Disambiguation

When multiple intents could match, Lex picks the highest confidence score. If two intents score similarly, you can configure clarification prompts — Lex asks "Did you mean X or Y?"

# Enable in console: Bot Settings > Clarification
Clarification prompt:
"I'm not sure what you mean. Did you want to (1) check order status or (2) place a new order?"
Max clarification retries: 2
If still unclear: trigger FallbackIntent

Closing Responses

Every intent should have a closing response for when it ends successfully. This replaces the plain "Intent fulfilled" default message.

# In Lex Console: Intent > Closing response
Message: "Your {PizzaSize} pizza has been ordered! ETA: 30 minutes. Enjoy! 🍕"

# You can also use session attributes in messages:
Message: "Order #{orderRef} confirmed for {CustomerName}. Check your email for details."

Slots & Slot Types

Data collection at the heart of every Lex conversation

Built-in Slot Types (AMAZON.*)

Slot TypeCapturesExample
AMAZON.DateDates in any format"next Monday" → 2025-01-20
AMAZON.TimeTimes"3pm" → 15:00
AMAZON.DurationTime spans"2 hours" → PT2H
AMAZON.NumberIntegers/floats"forty two" → 42
AMAZON.EmailAddressEmail strings[email protected]
AMAZON.PhoneNumberPhone numbers+1-800-555-0100
AMAZON.CityUS city namesNew York, Boston
AMAZON.AlphaNumericAny word/phraseFree-form string

Custom Slot Types

Two modes for custom types:

Expand Values

Lex recognizes your values plus similar words via NLU. Best for open-ended categories.

Values: ["pizza", "burger", "sushi"]
→ Also matches "pie", "sandwich"

Restrict to Slot Values

Lex only accepts exact matches + synonyms. Use for controlled data like product codes.

Values: ["PLAN_A", "PLAN_B"]
→ Rejects anything else

Slot Prompts & Retries

# Slot configuration in Lex console
Slot: DepartureCity
Type: AMAZON.City
Required: true

Prompts:
  "Which city are you flying from?"
  "What's your departure city?"      # Retry 1
  "Please tell me where you're departing from."  # Retry 2

Max retries: 2
If still not filled: Close with failure response

Slot Validation via Lambda

Use a validation hook to check slot values in real-time before the conversation proceeds:

import json

def lambda_handler(event, context):
    invocation_source = event['invocationSource']
    
    if invocation_source == 'DialogCodeHook':
        # Validate slots on every turn
        slots = event['sessionState']['intent']['slots']
        pizza_size = slots.get('PizzaSize')
        
        if pizza_size and pizza_size['value']['interpretedValue'] not in ['Small','Medium','Large']:
            # Invalid value — re-elicit the slot
            return elicit_slot(event, 'PizzaSize', 
                "Sorry, we only have Small, Medium, or Large. Which would you like?")
        
        # All good — delegate back to Lex
        return delegate(event)
    
    elif invocation_source == 'FulfillmentCodeHook':
        return fulfill_order(event)


def elicit_slot(event, slot_name, message):
    return {
        'sessionState': {
            'dialogAction': {'type': 'ElicitSlot', 'slotToElicit': slot_name},
            'intent': event['sessionState']['intent']
        },
        'messages': [{'contentType': 'PlainText', 'content': message}]
    }

def delegate(event):
    return {
        'sessionState': {
            'dialogAction': {'type': 'Delegate'},
            'intent': event['sessionState']['intent']
        }
    }

Fulfillment & Lambda

Connecting your bot to real backend systems

Two Fulfillment Modes

Static Response

Return a hardcoded message with slot values interpolated. No Lambda needed. Use for simple confirmations.

"Thanks {CustomerName}! Your order is placed."

Lambda Hook

Call a Lambda function with the full session state. Run business logic, query databases, call APIs.

Lambda → DynamoDB / Stripe / CRM

The Lambda Event Structure

Lex sends this JSON to your Lambda function. Understanding it is critical:

{
  "sessionId": "user-abc-123",
  "invocationSource": "FulfillmentCodeHook",  // or "DialogCodeHook"
  "inputTranscript": "Large pepperoni thin crust please",
  "sessionState": {
    "intent": {
      "name": "OrderPizza",
      "state": "ReadyForFulfillment",
      "slots": {
        "PizzaSize": {
          "value": {
            "originalValue": "large",
            "interpretedValue": "Large",
            "resolvedValues": ["Large"]
          }
        },
        "CrustType": {
          "value": {"interpretedValue": "Thin"}
        },
        "Toppings": {
          "value": {"interpretedValue": "Pepperoni"}
        }
      }
    },
    "sessionAttributes": {
      "userId": "user-abc-123"
    }
  }
}

Complete Fulfillment Lambda (Python)

import json
import boto3
from datetime import datetime
import uuid

dynamodb = boto3.resource('dynamodb')
orders_table = dynamodb.Table('PizzaOrders')

def lambda_handler(event, context):
    source = event['invocationSource']
    
    if source == 'FulfillmentCodeHook':
        slots = event['sessionState']['intent']['slots']
        
        # Extract slot values safely
        def get_slot(name):
            s = slots.get(name)
            return s['value']['interpretedValue'] if s else None
        
        size    = get_slot('PizzaSize')
        crust   = get_slot('CrustType')
        toppings = get_slot('Toppings') or 'Cheese only'
        
        # Generate order ID and save to DynamoDB
        order_id = str(uuid.uuid4())[:8].upper()
        orders_table.put_item(Item={
            'orderId': order_id,
            'size': size,
            'crust': crust,
            'toppings': toppings,
            'timestamp': datetime.utcnow().isoformat(),
            'status': 'confirmed'
        })
        
        message = f"Order #{order_id} confirmed! " \
                  f"Your {size} {crust} crust pizza with {toppings} " \
                  "will be ready in ~25 minutes."
        
        return close_intent(event, message, 'Fulfilled')
    
    return close_intent(event, "Something went wrong. Please try again.", 'Failed')


def close_intent(event, message, state):
    intent = event['sessionState']['intent']
    intent['state'] = state
    return {
        'sessionState': {
            'dialogAction': {'type': 'Close'},
            'intent': intent,
            'sessionAttributes': event['sessionState'].get('sessionAttributes', {})
        },
        'messages': [{'contentType': 'PlainText', 'content': message}]
    }
⚠️ Lambda Permission

You must add a resource-based policy to Lambda allowing Lex to invoke it. In Lex console under Alias → Lambda → select your function. AWS adds the permission automatically.

Conversation Flow

Visual flow designer, session state, and multi-intent conversations

Visual Flow Designer

Lex V2's Visual Conversation Builder lets you design branching dialogue without code. Access it via Bot → Visual conversation builder.

Start
Invoke Intent
Branch: Is Member?
↓ yes                            ↓ no
Member Discount Flow
Standard Pricing Flow

Session Attributes

Session attributes are key-value pairs that persist across the entire session (all intents). Use them to carry context:

# Lambda: Set a session attribute
session_attrs = event['sessionState'].get('sessionAttributes', {})
session_attrs['customerTier'] = 'GOLD'
session_attrs['cartTotal'] = '29.99'

# Return with updated session attributes
return {
    'sessionState': {
        'dialogAction': {'type': 'Delegate'},
        'intent': event['sessionState']['intent'],
        'sessionAttributes': session_attrs   # ← persisted!
    }
}

Request Attributes vs Session Attributes

TypeScopeUse Case
Session AttributesEntire session (all turns)User ID, cart, preferences
Request AttributesCurrent turn onlyUI context, page ID, A/B test variant

Switching Intents Mid-Conversation

Users often switch topics. Lex handles this via intent chaining. You can force a switch via Lambda:

# Lambda: switch to a different intent
return {
    'sessionState': {
        'dialogAction': {'type': 'ElicitIntent'},
        'intent': {'name': 'CheckOrderStatus'},  # jump here
        'sessionAttributes': session_attrs
    },
    'messages': [{
        'contentType': 'PlainText',
        'content': 'Sure! Let me check your order status. What\\'s your order number?'
    }]
}

Dialog Actions Reference

ActionEffect
DelegateHand control back to Lex to continue dialogue
ElicitSlotAsk user for a specific slot value
ElicitIntentAsk user what they want to do
ConfirmIntentAsk yes/no confirmation
CloseEnd the intent (fulfilled or failed)

Multi-Channel Deployment

Publishing your bot to web, Slack, Facebook, and Amazon Connect

Deployment Architecture

Web Chat (JS SDK)
+
Slack
+
Facebook Messenger
+
Amazon Connect
↓ all connect via
Lex Runtime API (V2)

Option A: Embedded Web Chat (AWS Amplify)

// Install: npm install @aws-sdk/client-lex-runtime-v2
import { LexRuntimeV2Client, RecognizeTextCommand } from "@aws-sdk/client-lex-runtime-v2";

const client = new LexRuntimeV2Client({
  region: "us-east-1",
  credentials: cognitoCredentials  // from Cognito Identity Pool
});

async function sendMessage(text) {
  const command = new RecognizeTextCommand({
    botId: "YOUR_BOT_ID",
    botAliasId: "TSTALIASID",  // or your alias
    localeId: "en_US",
    sessionId: "user-session-123",
    text: text
  });
  
  const response = await client.send(command);
  const reply = response.messages?.[0]?.content;
  console.log("Bot says:", reply);
  return reply;
}

// Usage
sendMessage("I want to order a large pizza");

Option B: Slack Integration

Create a Slack App

Go to api.slack.com/apps → Create New App → Enable Bot Token Scopes: chat:write, im:history.

Configure in Lex Console

Bot → Aliases → PROD → Add channel integration → Slack → Enter Bot Token + Signing Secret.

Set Slack Event URL

Copy the callback URL from Lex and paste into Slack App Event Subscriptions. Verify challenge.

Option C: Amazon Connect (Voice Bot)

This is the most powerful deployment — a full IVR / voice contact center with Lex as the NLU brain:

# Amazon Connect Contact Flow snippet (JSON)
{
  "Type": "GetParticipantInput",
  "Parameters": {
    "Text": "Welcome to PizzaBot! How can I help you today?",
    "LexV2Bot": {
      "AliasArn": "arn:aws:lex:us-east-1:123456789:bot-alias/BOTID/ALIASID"
    }
  }
}

Using API Gateway for Custom Web Integration

# Lambda proxy to Lex (keeps credentials server-side)
import boto3, json

lex = boto3.client('lexv2-runtime', region_name='us-east-1')

def lambda_handler(event, context):
    body = json.loads(event['body'])
    
    response = lex.recognize_text(
        botId='YOUR_BOT_ID',
        botAliasId='YOUR_ALIAS_ID',
        localeId='en_US',
        sessionId=body['sessionId'],
        text=body['message']
    )
    
    reply = response['messages'][0]['content'] if response.get('messages') else ''
    
    return {
        'statusCode': 200,
        'headers': {'Access-Control-Allow-Origin': '*'},
        'body': json.dumps({'reply': reply})
    }

Analytics & Testing

Measuring bot performance and catching failures before production

Built-in Analytics Dashboard

Lex console → Analytics shows:

📊 Conversation Metrics

  • Total conversations
  • Missed utterances
  • Intent success rate
  • Drop-off points

🎯 Intent Metrics

  • Per-intent recognition rate
  • Slot fill rate
  • Fulfillment success/failure
  • Avg. turns to complete

Missed Utterances — Your #1 Improvement Tool

The Missed Utterances report shows exactly what users said that Lex couldn't match. Review weekly and add them as utterances:

# Example missed utterances to review:
"gimme a pizza"         → Add to OrderPizza
"what's in my cart"     → Add to CheckCart intent
"cancel that"           → Add to CancelOrder
"speak to a person"     → Add to EscalateToAgent

Testing with the CLI (Automated)

# Test a Lex bot via AWS CLI
aws lexv2-runtime recognize-text \
  --bot-id YOUR_BOT_ID \
  --bot-alias-id TSTALIASID \
  --locale-id en_US \
  --session-id test-session-001 \
  --text "I want a large pepperoni pizza"

# Response shows intent + slots:
{
  "sessionId": "test-session-001",
  "messages": [{"content": "What type of crust would you like?"}],
  "sessionState": {
    "intent": {
      "name": "OrderPizza",
      "state": "InProgress",
      "slots": {
        "PizzaSize": {"value": {"interpretedValue": "Large"}},
        "Toppings": {"value": {"interpretedValue": "Pepperoni"}},
        "CrustType": null  # still being elicited
      }
    }
  }
}

Automated Test Script (Python)

import boto3, json

lex = boto3.client('lexv2-runtime')

test_cases = [
    {"input": "I want a pizza", "expected_intent": "OrderPizza"},
    {"input": "What's my order status", "expected_intent": "CheckOrderStatus"},
    {"input": "gibberish xyz 123", "expected_intent": "FallbackIntent"},
]

for i, test in enumerate(test_cases):
    resp = lex.recognize_text(
        botId='YOUR_BOT_ID', botAliasId='TSTALIASID',
        localeId='en_US', sessionId=f'test-{i}',
        text=test['input']
    )
    actual = resp['sessionState']['intent']['name']
    status = '✅ PASS' if actual == test['expected_intent'] else '❌ FAIL'
    print(f"{status} | '{test['input']}' → {actual}")

🧠 Quick Check

What should you do with "missed utterances" in Lex Analytics?
A. Delete them — they represent user errors
B. Ignore them until they accumulate to 1000+
C. Review and add them as utterances to the correct intents
D. Route all of them to FallbackIntent permanently
Missed utterances are gold. They show exactly how real users phrase things. Adding them regularly is the #1 way to improve bot accuracy over time.

Real-World Project

Build a complete Banking Support Bot — BankBot

This capstone project builds BankBot — a production-grade banking assistant that handles balance inquiries, transfers, and fraud alerts. It integrates everything you've learned.

BankBot Intent Map

IntentUtterances (sample)SlotsFulfillment
CheckBalance"What's my balance", "How much do I have"AccountTypeLambda → DynamoDB
TransferFunds"Transfer money", "Send $50 to savings"Amount, FromAccount, ToAccountLambda → Transfer API
ReportFraud"Report fraud", "My card was stolen"CardLast4, IncidentDateLambda → SNS alert
EscalateAgent"Talk to a human", "Agent please"Lambda → Connect queue

Full Lambda Architecture

import boto3, json

dynamodb = boto3.resource('dynamodb')
sns = boto3.client('sns')
accounts = dynamodb.Table('BankAccounts')

def lambda_handler(event, context):
    intent_name = event['sessionState']['intent']['name']
    source = event['invocationSource']
    slots = event['sessionState']['intent'].get('slots', {})
    session = event['sessionState'].get('sessionAttributes', {})
    
    def get_slot(name):
        s = slots.get(name)
        return s['value']['interpretedValue'] if s else None
    
    if intent_name == 'CheckBalance' and source == 'FulfillmentCodeHook':
        user_id = session.get('userId', 'demo-user')
        acct_type = get_slot('AccountType') or 'checking'
        
        # Look up balance
        result = accounts.get_item(Key={'userId': user_id, 'accountType': acct_type})
        balance = result.get('Item', {}).get('balance', 'N/A')
        
        return close(event, f"Your {acct_type} balance is ${balance:,.2f}.")
    
    elif intent_name == 'ReportFraud' and source == 'FulfillmentCodeHook':
        card = get_slot('CardLast4')
        # Send SNS alert to fraud team
        sns.publish(
            TopicArn='arn:aws:sns:us-east-1:123456789:FraudAlerts',
            Subject=f'Fraud Report - Card ending {card}',
            Message=json.dumps({'userId': session.get('userId'), 'card': card})
        )
        return close(event, 
            f"Fraud report filed for card ending {card}. "
            "Your card has been blocked. A specialist will contact you within 2 hours.")
    
    elif intent_name == 'TransferFunds' and source == 'FulfillmentCodeHook':
        amount = get_slot('Amount')
        from_acct = get_slot('FromAccount')
        to_acct = get_slot('ToAccount')
        # Real transfer logic goes here
        return close(event, f"${amount} transferred from {from_acct} to {to_acct}. Done! ✅")
    
    return close(event, "I'm not sure how to help with that. Type 'agent' to speak with someone.")


def close(event, message):
    intent = event['sessionState']['intent']
    intent['state'] = 'Fulfilled'
    return {
        'sessionState': {
            'dialogAction': {'type': 'Close'},
            'intent': intent,
            'sessionAttributes': event['sessionState'].get('sessionAttributes', {})
        },
        'messages': [{'contentType': 'PlainText', 'content': message}]
    }

Security Checklist for Production

  • Authentication: Pass user ID via session attributes from your auth layer (Cognito/JWT)
  • No PII in Lex logs: Disable conversation logs or use CloudWatch log filtering
  • IAM least-privilege: Lambda role should only access the specific DynamoDB tables it needs
  • Input validation: Always validate slot values in Lambda, never trust Lex output blindly
  • Rate limiting: Use API Gateway throttling on any public-facing Lex proxy endpoint
  • Bot versioning: Always deploy via aliases, never point directly to DRAFT in production
🚀 Lex + Bedrock (Next Level)

In your FallbackIntent Lambda, call Amazon Bedrock/Claude to handle questions Lex can't match. This creates a hybrid bot: structured tasks go through Lex intents, open-ended questions go to Claude. This is the modern agentic chatbot architecture.

🎓 Course Complete!

You now know: Lex core concepts, intent/slot/utterance design, Lambda fulfillment, conversation flow control, multi-channel deployment, analytics, and a real production architecture. Next step: build your own bot and integrate with Bedrock for LLM fallback.